package de.pollin.avr;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Bibliothek für Pollin AVR NetIO mit Standardfirmware
 * 
 * @version 1.0
 */
public class AvrNetIo
{

    private static final Logger LOG = LoggerFactory.getLogger(AvrNetIo.class);

    public static final int DEFAULT_PORT = 50290;

    private Socket client;

    private String hostnameOrIP;

    private int port;

    /**
     * Konstruktor
     * 
     * @param IP
     *        des NET-IO Boards
     */
    public AvrNetIo(String hostnameOrIP)
    {
        this(hostnameOrIP, DEFAULT_PORT);
    }

    /**
     * Konstruktor
     * 
     * @param IP
     *        des NET-IO Boards
     * @param port
     *        Port des Net-IO Boards ( Standard 50290)
     */
    public AvrNetIo(String hostnameOrIP, int port)
    {
        this.hostnameOrIP = hostnameOrIP;
        this.port = port;
    }

    /**
     * Stellt Verbindung zum NET-IO Board her Timeout 10000ms
     * 
     * @return true: Verbindung hergestellt, false Fehler
     */
    public boolean connect()
    {
        boolean connected = false;
        try
        {

            client = new Socket();
            client.connect(new InetSocketAddress(hostnameOrIP, port), 10000);
            connected = true;

        }
        catch (UnknownHostException ex)
        {
            LOG.error("The given ip-address or hostname is unknown.", ex);
        }
        catch (IOException ex)
        {
            LOG.error("Error while connecting to microcontroller.", ex);
        }
        return connected;
    }

    /**
     * Disconnect Methode
     */
    public void disconnect()
    {
        try
        {
            client.close();
        }
        catch (IOException ex)
        {
            LOG.error("Error while disconnecting.", ex);
        }
    }

    /**
     * Gibt den Verbindungsstatus zurück
     * 
     * @return Status
     */
    public boolean isConnected()
    {
        return client != null && client.isConnected();
    }

    /**
     * Schickt Daten zum Board
     * 
     * @param command
     *        Daten
     * @return Antwort
     */
    private String sendData(String command)
    {
        String resultLineFromController = "";
        if (!isConnected())
        {
            throw new IllegalStateException("You have to connect to the controller, before you can send data.");
        }

        try
        {
            PrintWriter outputStreamToController = new PrintWriter(client.getOutputStream());
            outputStreamToController.println(command + "\r\n");
            outputStreamToController.flush();

            BufferedReader inputStreamFromController = new BufferedReader(new InputStreamReader(client.getInputStream()));
            resultLineFromController = inputStreamFromController.readLine();
            resultLineFromController = resultLineFromController.replaceAll("\0", "").replaceAll("\r\n", "").trim();
        }
        catch (IOException ex)
        {
            LOG.error("Error while interacting with controller.", ex);
        }
        return resultLineFromController;
    }

    /**
     * Gibt den Wert eines ADC-Port zurück (ADC Port (1-4))
     * 
     * @param port
     *        Port
     * @return ADC Wert (-1 : Fehler)
     */
    public int getADC(int port)
    {
        if (port < 1 || port > 4)
        {
            return -1;
        }

        return Integer.parseInt(sendData("GETADC " + port));
    }

    /**
     * Setzt den Wert eines Ausgangsports
     * 
     * @param port
     *        Port (1-8)
     * @param level
     *        true= high, false = low
     */
    public void setOutPort(int port, boolean level)
    {
        if (port < 1 || port > 8)
        {
            throw new IllegalArgumentException("The portnumber must be between 1 and 8 but you choose: " + port);
        }

        char levelchar = level ? '1' : '0';

        checkControllerResult(sendData("SETPORT " + port + "." + levelchar));
    }

    /**
     * Checks the result from the controller.
     * 
     * @param resultDataFromController
     *        the value which comes back from the controller
     */
    private void checkControllerResult(String resultDataFromController)
    {
        if (!resultDataFromController.equals("ACK"))
        {
            throw new IllegalStateException("The Controller does not answered with ACK - acknowledged.");
        }
    }

    /**
     * Gibt den Wert eines Ausgangs zurück (1-8)
     * 
     * @param port
     *        Port (1-8)
     * @return true = high, false = low
     */
    public boolean getOutPort(int port)
    {

        if (port < 1 || port > 8)
        {
            throw new IllegalArgumentException("The portnumber must be between 1 and 8 but you choose: " + port);
        }

        int positionInStatusByte = 1 << (8 - port);

        short statusByte = getStatus();

        return (statusByte & positionInStatusByte) == positionInStatusByte;
    }

    /**
     * Liefert das Ergebnis von GETSTATUS zurück
     * 
     * @return ******** * = Portwert ( 1 oder 0)
     */
    public short getStatus()
    {
        String invertedStatusBits = sendData("GETSTATUS");

        if (invertedStatusBits.isEmpty())
        {
            throw new IllegalStateException("The answer is empty.");
        }

        invertedStatusBits = invertedStatusBits.substring(1);

        String statusBits = new StringBuffer(invertedStatusBits).reverse().toString();

        return Short.valueOf(statusBits, 2).shortValue();
    }

    /**
     * Gibt die IP des Boards zurück
     * 
     * @return IP
     */
    public String getIP()
    {
        return sendData("GETIP");
    }

    /**
     * Setzt die IP des Boards Reset des Boards notwendig
     * 
     * @param ip
     *        IP (xxx.xxx.xxx.xxx)
     */
    public void setIP(String ip)
    {
        checkControllerResult(sendData("SETIP " + ip));
    }

    /**
     * Liefert die Netzmaske des Boards zurück
     * 
     * @return Netzmaske (xxx.xxx.xxx.xxx)
     */
    public String getMask()
    {
        return sendData("GETMASK");
    }

    /**
     * Setzt die Netzmaske des Boards
     * 
     * @param mask
     *        Neue Maske (xxx.xxx.xxx.xxx)
     */
    public void setMask(String mask)
    {
        checkControllerResult(sendData("SETMASK " + mask));
    }

    /**
     * Gibt den Gateway des Boards zurück
     * 
     * @return Gateway (xxx.xxx.xxx.xxx)
     */
    public String getGW()
    {
        return sendData("GETGW");
    }

    /**
     * Setzt den Gateway
     * 
     * @param gw
     *        (xxx.xxx.xxx.xxx)
     */
    public void setGW(String gw)
    {
        checkControllerResult(sendData("SETGW " + gw));
    }

    /**
     * Initialisiert das LCD
     */
    public void initLCD()
    {
        checkControllerResult(sendData("INITLCD"));
    }

    /**
     * Schreibt einen Text auf das LCD in eine bestimmte Zeile
     * 
     * @param line
     *        Zeile (1 oder 2)
     * @param txt
     *        Text der geschrieben werden soll
     */
    public void writeLCD(int line, String txt)
    {
        if (line == 1 || line == 2)
        {
            checkControllerResult(sendData("WRITELCD " + line + "." + txt));
        }
        else
        {
            throw new IllegalArgumentException("You have to choose line 1 or 2.");
        }
    }

    /**
     * Löscht das LCD
     * 
     * @param line
     *        Zeilennummer ( 1 oder 2 , sonst alle)
     */
    public void clearLCD(int line)
    {
        if (line == 1 || line == 2)
        {
            sendData("CLEARLCD " + line);
        }
        else
        {
            sendData("CLEARLCD");
        }
    }

    /**
     * Gibt die Version zurück
     * 
     * @return Version
     */
    public String getVersion()
    {
        return sendData("VERSION");
    }

}
